/*
 * Copyright:  2018 smarabbit studio.
 *
 *  Licensed under the Confluent Community License; you may not use this file
 *  except in compliance with the License.  You may obtain a copy of the License at
 *
 *  http://www.confluent.io/confluent-community-license
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *  WARRANTIES OF ANY KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations under the License.
 *
 *  @作   者： 黄开晖 (117227773@qq.com)
 *  @日   期:  2021年5月23日
 */
package com.massyframework.beanskin.graphql.util.expression;

import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;

import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import graphql.schema.DataFetchingEnvironment;

/**
 * {@link ExpressionPredicate},使用表达式做为断言
 * @author huangkh
 *
 */
public class ExpressionPredicate<T> implements Predicate<T> {
	private static final String DEFAULT_NAME = "item";
	
	private static final Logger LOG = LoggerFactory.getLogger(ExpressionPredicate.class);
	
	private ScriptEngine engine;
	private CompiledScript script;
	
	private String name;
	private String exp;

	/**
	 * 使用{@code builder}创建新实例
	 * {@link Builder}
	 * @throws ScriptException 
	 */
	protected ExpressionPredicate(Builder<T> builder) throws ScriptException {
		this.engine = Objects.requireNonNull(builder.engine, "\"engine\" cannot be null.");
		this.name = builder.targetName;
		
		if (builder.exp != null) {
			if (builder.exp.indexOf(this.name) == -1) {
				throw new IllegalArgumentException("cannot found \"" + this.name + "\" variable with: \"" + this.exp + "\"");
			}
		}
		
		Compilable compEngine = (Compilable)engine;
		this.script = compEngine.compile(builder.exp);
	}
	
	/**
	 * 设置变量
	 * @param name {@link String},变量名
	 * @param value {@link Object},变量值
	 */
	public void setVariable(String name, Object value) {
		this.engine.put(name, value);
	}

	@Override
	public boolean test(T t) {
		if (this.script == null) return false;
		this.engine.put(this.name, t);
		try {
			return (boolean)this.script.eval();
		} catch (ScriptException e) {
			if (LOG.isErrorEnabled()) {
				LOG.error(e.getMessage(), e);
			}
			throw new RuntimeException(e.getMessage(), e);
		}
	}
	
	/**
	 * 创建构建器
	 * @param requireType {@link Class},列表对象的类型
	 * @return {@link Builder}
	 */
	public static <T> Builder<T> newBuilder(Class<T> requireType){
		return new Builder<>();
	}
	
	/**
	 * 根据{@code env}和{@code name},创建对应的ExpressionPredicate
	 * @param <T>
	 * @param env {@link DataFetchingEnvironment}，环境
	 * @param name {@link String},表达式参数名.
	 * @param engine {@link ScriptEngine}
	 * @param requireType {@link Class},列表对象的类型
	 * @return {@link ExpressionPredicate}
	 * @throws {@link ScriptException}
	 */
	public static <T> ExpressionPredicate<T> newInstance(DataFetchingEnvironment env, String name, ScriptEngine engine,
			Class<T> requireType) throws ScriptException{
		Builder<T> result = new Builder<T>();
		result.scriptEngine(engine);
		Map<String, Object> map = env.getArgument(name);
		if (map != null) {
			Object value = map.get("value");
			if (value != null) {
				result.expression(value.toString());
			}
			
		}
		
		return result.build();
	}

	/**
	 * 构建器
	 */
	public static class Builder<T> {
		
		protected ScriptEngine engine;
		protected String exp;
		protected String targetName = DEFAULT_NAME;
		
		/**
		 * 设置脚本引擎
		 * @param value {@link ScriptEngine},脚本引擎
		 * @return {@link Builder}
		 */
		public Builder<T> scriptEngine(ScriptEngine value) {
			this.engine  = value;
			return this;
		}
		
		/**
		 * 设置表达式公式
		 * @param expression {@link String}
		 * @return {@link Builder}
		 */
		public Builder<T> expression(String expression) {
			this.exp = expression;
			return this;
		}
				
		/**
		 * 设置目标名称
		 * @param name {@link String}
		 * @return {@link Builder}
		 */
		public Builder<T> targetName(String name) {
			this.targetName = name;
			return this;
		}
		
		public ExpressionPredicate<T> build() throws ScriptException{
			return new ExpressionPredicate<>(this);
		}
	}
}
